﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace fsplit
{
    public class DissolutionPlan
    {
        public string SourceName;
        public ulong SourceSize;
        public uint PartCount;
        public uint RemainBytes;
        public uint PartSize;
    };
    public class Part
    {
        public ushort part_id;
        public string filename;
        public uint part_size;
    }
    public interface ITarget
    {
        IList<Part> Parts { get; }
        Stream CreateDestination(uint total, int part_size);
    }
    public class FileTarget : ITarget
    {
        private string label;
        private List<Part> parts;
        public FileTarget(string label)
        {
            parts = new List<Part>();
            this.label = label;
        }
        public IList<Part> Parts
        {
            get { return parts; }
        }

        public Stream CreateDestination(uint total, int part_size)
        {
            ushort part_id = (ushort)(parts.Count + 1);
            string output_file = string.Format("{0}_part{1:0#}_of_{2}.dat", label, part_id, total);
            parts.Add(new Part() { part_id = part_id, part_size = (uint)part_size, filename = output_file });
            return new FileStream(output_file, FileMode.Create, FileAccess.Write);
        }
    }
    public class SplitResult
    {
        public string ResultLabel;
        public DissolutionPlan Plan;
        public ITarget Target;
    }
    public class Fmerge
    {
        public const uint _1Kb = 1024;
        public const uint _1Mb = 1048576; // _1Kb * 1024

        public static DissolutionPlan GetDissolutionPlan(ulong total_size, uint split_size)
        {
            if (total_size == 0LU)
            {
                throw new ArgumentOutOfRangeException("total_size");
            }
            if (split_size == 0U)
            {
                throw new ArgumentOutOfRangeException("split_size");
            }
            ulong part_count = total_size / (ulong)split_size;
            ulong remain = total_size % (ulong)split_size;
            ulong total_part_count = remain != 0 ? part_count + 1 : part_count;
            split_size = total_size > split_size ? split_size : (uint)total_size;
            return new DissolutionPlan() { SourceSize = total_size, PartCount = (uint)total_part_count, RemainBytes = (uint)remain, PartSize = split_size };
        }

        public static SplitResult Split(string filename, uint split_size)
        {
            var file = new FileInfo(filename);
            if (!file.Exists)
            {
                throw new FileNotFoundException("Input file must exist.", filename);
            }
            return Split(file, split_size);
        }
        public static SplitResult Split(FileInfo file, uint split_size)
        {
            string label = Path.GetFileNameWithoutExtension(file.FullName);
            using (var source = file.OpenRead())
            {
                return Split(source, split_size, new FileTarget(label), file.FullName);
            }
        }
        public static SplitResult Split(Stream source, uint split_size, ITarget target, string filename)
        {
            DissolutionPlan plan = GetDissolutionPlan((ulong)source.Length, split_size);
            plan.SourceName = filename;
            for (uint k = 0; k < plan.PartCount; ++k)
            {
                int part_size = (int)(k == plan.PartCount - 1 && plan.RemainBytes > 0U ? plan.RemainBytes : plan.PartSize);
                byte[] part = new byte[part_size];
                source.Read(part, 0, part_size);
                using (Stream destination = target.CreateDestination(plan.PartCount, part_size))
                {
                    destination.Write(part, 0, part_size);
                }
            }
            return new SplitResult() { ResultLabel = string.Format("Created {0} parts:", plan.PartCount), Target = target, Plan = plan };
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                if (args.Length != 2)
                {
                    Console.WriteLine("fsplit <file> <split_size>");
                    return;
                }

                string filename = args[0];
                uint split_size = uint.Parse(args[1]);// *Fmerge._1Mb;
                var result = Fmerge.Split(filename, split_size);

                string sourcefilename = Path.GetFileNameWithoutExtension(result.Plan.SourceName);

                Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
                string logname = string.Format("{0}_{1}.log", sourcefilename, DateTime.Now.ToString("MMMdd-hhmmss-ff"));
                var listenerFile = new FileInfo(logname);
                var listenerWriter = listenerFile.CreateText();
                var listener = new TextWriterTraceListener(listenerWriter);
                Trace.Listeners.Add(listener);
                Trace.AutoFlush = true;

                Trace.WriteLine(string.Format("{0:N0} : {1}\n\n{2}\n{3}\n={4:N0}\n", result.Plan.SourceSize, result.Plan.SourceName, result.ResultLabel, result.Target.Parts.Aggregate(new StringBuilder(), (whole, next) => whole.AppendFormat("part {0:0#} size: {1:N0}\n", next.part_id, next.part_size)), result.Target.Parts.Sum(p => p.part_size)));
                string mergefile = string.Format("{0}_merge.cmd", sourcefilename);
                using (var writer = File.CreateText(mergefile))
                {
                    writer.WriteLine(string.Format("copy /B {0} {1}_merged.dat", result.Target.Parts.Aggregate(new StringBuilder(), (whole, next) => whole.AppendFormat("+{0}", next.filename)).ToString().Substring(1), sourcefilename));
                }
                Trace.WriteLine(string.Format("merge:\t{0}", mergefile));
                Trace.WriteLine(string.Format("log:\t{0}", logname));
            }
            catch (Exception ex) { Trace.WriteLine(string.Format("{0}: {1}", ex.GetType().FullName, ex.Message)); }
        }
    }
}